/*
    Contact info:
    bhepple@freeshell.org
    http://bhepple.freeshell.org/scrmgr

    Copyright (C) 1991-2008 Bob Hepple

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; see the file COPYING.  If not, write to
    the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
    Boston, MA 02111-1307, USA.

*/
/* smgetfl.c ... file picker */

/* Use a window to select a filename. 

   This routine uses the 'filename' string as a starting pattern (as
   in sh(1)).  Anything passed in through 'direct' is used as an initial
   directory.

   If the user presses '/' then this routines trys to 'cd' to that
   directory. In the display of files, directories are marked with a
   trailing '/'. The user may enter wildcard characters a la shell - *,
   ?, [x-y]; the files listed in the window are then sub-setted. Any
   'msg' is used as a primary prompt.

*/

#define SM_CLASS extern
#include "smlib.h"
#include "smio.h"

#ifdef BSD_DIRENT
#include <sys/types.h>
#include <dirent.h>
#define DIRECT_ENT struct dirent
#endif

#ifdef SYSV_DIRENT
#include <sys/types.h>
#include <dirent.h>
#define DIRECT_ENT struct dirent
#endif

#ifdef TC_NDIR
#include <ndir.h>
#define DIRECT_ENT struct direct
#endif

#ifdef XENIX_NDIR
#include <sys/types.h>
#include <sys/dirent.h>
#include <sys/ndir.h>
#define DIRECT_ENT struct direct
#endif

#include <unistd.h>
#include <sys/stat.h> /* for getfilename */

CHAR *smgetfl_rcsid(void) {
    return("$Id: smgetfl.c,v 1.7 2008/02/19 13:15:50 bhepple Exp $");
}

#define SM_SETBIT8(x) (x | 0200)
#define SM_ALL SM_SETBIT8('*')
#define SM_ONE SM_SETBIT8('?')
#define SM_SET SM_SETBIT8('[')
#define SM_RNG SM_SETBIT8('-')
#define SM_END SM_SETBIT8(']')

static struct {
    CHAR	getfilename_filename[MAXPATHLEN + 1];
} global = {
    ""
};

struct getfilename_s {
    f_ptr	destructor;
    CHAR 	*direct;
    CHAR	*prompt_direct;
    CHAR	*init_direct;
    ARRAY	sub_data;
    ARRAY	master_data;
    INDEX	index;
    CHAR	*file_mask;
    CHAR	*file_maskp;
    CHAR	*file_umask;
    CHAR	*fnmsg;
    int		window;
    SM_SCREEN	*dummy_scrp;
    SM_SOFTKEY	store_skey;
    int		dir_only;
    int		disallow_chdir;
};

int getfilename_destructor ARG1(struct getfilename_s *, getfilename)
{
    if (getfilename->master_data) {
	f_ptr	f;
	f = (f_ptr) free;
	removeAllItemsFromArray(getfilename->master_data, f);
	freeArray(getfilename->master_data);
    }
    /* elements of sub_data are shared with getfilename->master_data 
       and do not need freeing again */
    if (getfilename->sub_data)
	freeArray(getfilename->sub_data);

    /* dummy_scr->data is just a pointer to sub_data - no need to de-alloc */
    (*(getfilename->dummy_scrp))->data = NULL;
    sm_destroy_scr(getfilename->dummy_scrp);

    /* sm_del_win(getfilename->window);  delete help window */
    sm_message("");

    free(getfilename->file_mask);
    free(getfilename->file_umask);
    free(getfilename->prompt_direct);
    free(getfilename->direct);
    free(getfilename->init_direct);
    free(getfilename->fnmsg);

    free(getfilename);
    return(0);
}

static void sm_compile_filespec ARG2(unsigned char *, buf, CHAR *, pat)
{
    CHAR	c;

    c= *pat;

    while ((c= *pat++) != 0) {
        switch (c) {

        case '\\':
            if ((c= *pat++) != 0 && strchr("\\*?[", c) != NULL)
                *buf++ = c;
            else {
                *buf++ = '\\';
                --pat;
            }
            break;

        case '*':
            *buf++ = (unsigned char)SM_ALL;
            break;

        case '?':
            *buf++ = (unsigned char)SM_ONE;
            break;

        case '[':
            if (*pat == 0 || strchr(pat+1, ']') == NULL) {
                *buf++ = c;
                break;
            }
            *buf++ = (unsigned char)SM_SET;
            c= *pat++;
            do {
                *buf++ = c;
                if (*pat == '-' && (c= pat[1]) != ']') {
                    *buf++ = (unsigned char)SM_RNG;
                    *buf++= c;
                    pat += 2;
                }
            } while ((c= *pat++) != ']');
            *buf++ = (unsigned char)SM_END;
            break;

        default:
            *buf++ = c;
            break;

        }
    }
    *buf= 0;
}

/* Pattern matching function for filenames */
/* Each occurrence of the * pattern causes a recursion level */
static int sm_match ARG2(CHAR *, name, unsigned char *, pat)
{
    int 	c, k;
    int		ok;

    while ((c = *pat++) != 0) {
        switch (c) {

        case SM_ONE:
            if (*name++ == 0)
                return(FALSE);
            break;

        case SM_ALL:
            if (*pat == 0)
                return(TRUE);
            for (; *name != 0; ++name) {
                if (sm_match(name, pat))
                    return(TRUE);
            }
            return(FALSE);

        case SM_SET:
            ok= 0;
            k= *name++;
            while ((c = *pat++) != SM_END) {
                if (*pat == SM_RNG) {
                    if (c <= k && k <= pat[1])
                        ok= 1;
                    pat += 2;
                }
                else if (c == k)
                    ok= 1;
            }
            if (!ok)
                return(FALSE);
            break;

        default:
            if (*name++ != c)
                return(FALSE);
            break;

        }
    }
    return(*name == 0);
}

/* Create a new list of files that match pattern 'mask'. If 'mask' contains no
   wildcards, then add a trailing '*'. Return the resulting pattern (usually
   for display purposes) in umask. */
static  ARRAY sm_subset_files ARG3(ARRAY, data, CHAR *, mask, CHAR *, umask)
{
    CHAR		**p, store, *filetype, *alt;
    ARRAY		sub_data;
    unsigned char 	pattern[200], *pp;
    int			add_star = 1;

    /* Support one simple '|' or command - this needs generalising! */
    if ((alt = strchr(mask, '|')) != 0)
	*alt++ = 0;
	
    sm_compile_filespec(pattern, mask);
    for (pp = &pattern[0]; *pp; pp++)
	if (*pp & SM_SETBIT8(*pp)) { /* a META- character, denoting a wildcard */
	    add_star = 0;
	    break;
	}

    strcpy(umask, mask);

    if (add_star)
	strcat(umask, "*");

    sm_compile_filespec(pattern, mask);

    sub_data = NULL;
    for (p = (CHAR **)firstArrayItemp(data); 
	 p && *p; 
	 p = (CHAR **)nextArrayItemp(data, (void **)p)) {
	filetype = SM_LASTCHAR(*p);
	store = *filetype;
	*filetype = 0;
	if (store == '/')
	    sub_data = addToArray(sub_data, *p);
	else if (sm_match(*p, pattern))
	    sub_data = addToArray(sub_data, *p);
	else if (alt) {
	    sm_compile_filespec(pattern, alt);
	    if (sm_match(*p, pattern))
		sub_data = addToArray(sub_data, *p);
	    sm_compile_filespec(pattern, umask);
	}
	*filetype = store;
    }

    if (alt)
	*--alt = '|'; /* hack city */

    return(sub_data);
}

static void getfilename_msg ARG1(struct getfilename_s *, getfilename)
{
    CHAR	*buf = alloca(MAXPATHLEN + 1);
    int		len;

#define MaxMsgLen 80

    /* Prompt the user */
    len = strlen(getfilename->fnmsg) +
	  strlen(getfilename->prompt_direct) +
	  strlen(getfilename->file_umask);

    strcpy(buf, getfilename->fnmsg);

    if (len > MaxMsgLen) {
	strcat(buf, "...");
	len = MaxMsgLen - (strlen(getfilename->fnmsg) +
			   strlen(getfilename->file_umask) + 3);
	if (len > 0)
	    strcat(buf, &getfilename->prompt_direct[strlen(getfilename->prompt_direct) - len]);
    } else
	strcat(buf, getfilename->prompt_direct);

    strcat(buf, getfilename->file_umask);
    sm_message(buf);
}

static int sm_getfilename_helper ARG6(int, 			command,
				      ARRAY *,	 		data,
				      INDEX *, 			index,
				      int, 			row,
				      int, 			col,
				      struct getfilename_s *,	getfilename)
{
    CHAR	*p;
    int		mask_changed = 0;

    if (command == '/') {
    } else if ((command >= ' ') && (command <= '~')) {
	if (smio_insertp())
	    for (p = strchr(getfilename->file_maskp, 0) + 1;
		 p > getfilename->file_maskp;
		 p--)
		*p = *(p - 1);
	if (*getfilename->file_maskp == 0)
	    *(getfilename->file_maskp + 1) = 0;
	*getfilename->file_maskp++ = command;
	mask_changed = 1;
	command = 0;
    } else
	switch (command) {
        case SM_DEL_BACK_C:
	    if (getfilename->file_maskp > getfilename->file_mask) {
		getfilename->file_maskp--;
		for (p = getfilename->file_maskp; *p; p++)
		    *p = *(p + 1);
		mask_changed = 1;
	    }
	    command = 0;
	    break;

	case SM_HOME_FIELD_C:
	    getfilename->file_maskp = getfilename->file_mask;
	    command = 0;
	    break;

	case SM_END_FIELD_C:
	    getfilename->file_maskp = strchr(getfilename->file_mask, 0);
	    command = 0;
	    break;

	case SM_CLEAR_FIELD_C:
	    *getfilename->file_maskp = 0;
	    mask_changed = 1;
	    command = 0;
	    break;

	case SM_DEL_CHAR_C:
	    for (p = getfilename->file_maskp; *p; p++)
		*p = *(p + 1);
	    mask_changed = 1;
	    command = 0;
	    break;

	case SM_LEFT_C:
	    if (getfilename->file_maskp > getfilename->file_mask)
		getfilename->file_maskp--;
	    command = 0;
	    break;

	case SM_RIGHT_C:
	    if (*getfilename->file_maskp)
		getfilename->file_maskp++;
	    command = 0;
	    break;

	/* These are all the (mainly movements) commands that sm_selection
	   would otherwise handle ... do them here instead */
	case SM_UP_C:
	case SM_DOWN_C:
	case SM_HOME_SCREEN_C:
	case SM_END_SCREEN_C:
	case SM_FIRST_SCREEN_C:
	case SM_LAST_SCREEN_C:
	case SM_NEXT_SCREEN_C:
	case SM_PREV_SCREEN_C:
	case SM_MOUSE_EVENT_C:
	case SM_QUIT_C:
	case SM_NEXT_FIELD_C:
	case SM_NEXT_LINE_C: /* Carriage return */
	    break;

	default:
	    command = 0;
	    break;
 	}

    if (mask_changed) {
	if (*data)
	    freeArray(*data);
	getfilename->sub_data = 
	    *data = sm_subset_files(getfilename->master_data,
				    getfilename->file_mask,
				    getfilename->file_umask);
	*index = 0;
    }

    getfilename_msg(getfilename);
    return(command);
}

/* Sort into directories then files. Then sort into alphabetic order */
static int getfilename_sort ARG2(const void *, a1, const void *, a2)
{
    if ((*(SM_LASTCHAR(*(CHAR **)a1)) == '/') &&
	(*(SM_LASTCHAR(*(CHAR **)a2)) != '/'))
	return(-1);
    else if ((*(SM_LASTCHAR(*(CHAR **)a2)) == '/') && 
	     (*(SM_LASTCHAR(*(CHAR **)a1)) != '/'))
	return(1);
    else
	return(strcmp(*(CHAR **)a1, *(CHAR **)a2));
}

static int prep_getfilename ARG1(struct getfilename_s *, getfilename)
{
    CHAR	*p, *buf = alloca(MAXPATHLEN + 1);
    INDEX	num_files;
    DIR		*dirp;
    DIRECT_ENT	*directory_entry;
    struct stat	sbuf;
    DIR		*opendir();
    DIRECT_ENT	*readdir();

    /* Expand ~/ and ~user/ if necessary */
    fqn(buf, getfilename->direct);
    if (chdir(buf) < 0) {
	getfilename->file_mask = NULL;
	sm_message("Can't chdir there");
	return(SM_ERROR);
    }

    /* Store current directory for prompting (in sm_getfilename_helper) */
    strcpy(getfilename->prompt_direct, getfilename->direct);
    if (*getfilename->prompt_direct &&
	*SM_LASTCHAR(getfilename->prompt_direct) != '/')
	strcat(getfilename->prompt_direct, "/");

    if (((p = strstr(getfilename->prompt_direct,
		    getfilename->init_direct)) != NULL) && 
	(p == getfilename->prompt_direct)) {
	/* Remove unecessary pwd from prompt */
	CHAR *d = p;
	p += strlen(getfilename->init_direct);
	if (*p == '/')
	    p++;
	*d = 0;
	while ((*d++ = *p++));
    }

    if ((dirp = opendir(".")) == NULL) {
	sm_error("Can't open directory '.'");
	getfilename->file_mask = NULL;
	return(SM_ERROR);
    }

    if (getfilename->master_data) {
	f_ptr	f;
	f = (f_ptr) free;

	removeAllItemsFromArray(getfilename->master_data, f);
	freeArray(getfilename->master_data);
    }

    /* sub_data just points to some or all of the strings in master */
    if (getfilename->sub_data)
	freeArray(getfilename->sub_data);

    /* Get all the files in this directory */
    sm_message("Reading files, please wait ...");
    while ((directory_entry = readdir(dirp)) != NULL) {
	if (strcmp(directory_entry->d_name, ".")) {

	    CHAR	*filename;

	    /* Skip on following:					  */
	    /*   							  */
	    /* DIR	File	Disallow_chdir	dir_only	Name='..' */
	    /* Y	N	Y		N		?	  */
	    /* Y	N	Y		Y		Y	  */
	    /* N	Y	?		Y		? 	  */

	    stat(directory_entry->d_name, &sbuf);
	    if (sbuf.st_mode & S_IFDIR) {
		if (getfilename->disallow_chdir) {
		    if (getfilename->dir_only) {
			if (strcmp(directory_entry->d_name, "..") == 0)
			    continue;
		    } else
			continue;
                }
	    } else { /* Not a directory */
		if (getfilename->dir_only)
		    continue;
	    }

	    filename = xmalloc((INDEX) (strlen(directory_entry->d_name) + 2));
	    strcpy(filename, directory_entry->d_name);
	    /* Don't put the trailing '/' if dir_only is set so that the
	       directory will be selected as an item instead of chdir() to it */
	    if (!getfilename->dir_only && (sbuf.st_mode & S_IFDIR))
		strcat(filename, "/");
#ifdef ROOT_IS_A_REGULAR_FILE /* MS-DOG thinks '/' is a regular file. Sigh. */
	    else if ((strcmp(filename, "..") == 0) 
		     && ((p = strchr(getfilename->prompt_direct, '/')) != NULL)
		     && (strchr(p+1, '/') == NULL))
		strcat(filename, "/");
#endif
	    else
		strcat(filename, " ");

	    getfilename->master_data = addToArray(getfilename->master_data, filename);

	}
    }
    closedir(dirp);

    /* Sort them */
    num_files = arrayLength(getfilename->master_data);
    if (num_files == 0) 
	sm_message("No files");
    else {
	sm_message("Sorting files, please wait...");
	qsort((char **) firstArrayItemp(getfilename->master_data),
	      (int) num_files, 
	      sizeof(CHAR **), 
	      getfilename_sort);
	
	getfilename->index = 0;
	
	/* Find the subset that match the pattern mask - if the mask does not
	   contain any wildcards then add a trailing '*' */
	getfilename->sub_data = sm_subset_files(getfilename->master_data, 
						getfilename->file_mask, 
						getfilename->file_umask);
	
	getfilename_msg(getfilename);
	
	/* Now let him scroll and select files or directories, and/or refine
	   the pattern mask */
	(*(getfilename->dummy_scrp))->data = getfilename->sub_data;
	(*(getfilename->dummy_scrp))->num_records = arrayLength(getfilename->sub_data);
	
	sm_setup_selection(*(getfilename->dummy_scrp), 
			   &getfilename->index,
			   0, 
			   10,
			   sm_getfilename_helper,
			   getfilename,
			   sm_getfilename_skey);
    }
    return(0);
}

CALLBACK(do_getfilename)
{
    struct getfilename_s *getfilename = (struct getfilename_s *) callback_data;
    int			i, finished = TRUE;
    CHAR		*filename;

    if (command == SM_YES_C) {
	if (getfilename->sub_data &&
	    (getfilename->index >= 0) &&
	    (filename = arrayItem(getfilename->sub_data, getfilename->index)) &&
	    (*SM_LASTCHAR(filename) == '/')) {
	    
	    /* We have a new directory */
	    i = strlen(getfilename->direct);
	    if (i) i--;
	    if (strcmp(filename, "../") == 0) {
		/* Go up 1 direct */
		if (getfilename->direct[i] == '/')
		    i--;
		while (i && (getfilename->direct[i] != '/'))
		    i--;
#ifdef USE_DRIVE_LETTER
		if (i > 2) /* direct contains C: */
		    getfilename->direct[i] = 0;
		else
		    getfilename->direct[3] = 0;
#else
		if (i > 0)
		    getfilename->direct[i] = 0;
		else /* We're looking at '/' */
		    getfilename->direct[1] = 0;
#endif
		
		/* Re-insert a '/' on ~/ and ~user/ */
		if ((getfilename->direct[0] == '~') && 
		    (strchr(getfilename->direct, '/') == NULL))
		    strcat(getfilename->direct, "/");
		
	    } else { /* Add directory to the current one */
		if (getfilename->direct[i] != '/')
		    strcat(getfilename->direct, "/");
		strcat(getfilename->direct, filename);
	    }
	    getfilename->file_maskp = getfilename->file_mask;
	    finished = FALSE;
	} else { /* It's not a directory */
	    /* User wants this file */
	    if ((getfilename->index >= 0) && getfilename->sub_data) {
		CHAR *filetype = SM_LASTCHAR(filename);

		if ((*filetype == '/') || (*filetype == ' '))
		    *filetype = 0;

		fqn(getfilename->file_mask, filename);
	    } /* else, Give him back whatever is in file_mask ... literally */
	}
    } else {
	*getfilename->file_mask = 0;
	chdir(getfilename->init_direct);
    } 
    
    strcpy(global.getfilename_filename, getfilename->file_mask);

    if (finished) {
	sm_paint_skey(getfilename->store_skey);
	return(command);
    } else {
	prep_getfilename(getfilename);
	return(0);
    }
}

int sm_setup_getfilename ARG5(CHAR *, 	filename,
			      CHAR *,	direct,
			      CHAR *, 	msg,
			      int,	dir_only,
			      int, 	disallow_chdir)
{
    struct getfilename_s 	*getfilename;

    getfilename =(struct getfilename_s *)xmalloc((INDEX) sizeof(struct getfilename_s));
    getfilename->destructor 	= getfilename_destructor;
    getfilename->file_mask 	= xmalloc((INDEX) (MAXPATHLEN + 1));
    getfilename->file_maskp 	= getfilename->file_mask;
    getfilename->file_umask 	= xmalloc((INDEX) (MAXPATHLEN + 1));
    getfilename->prompt_direct 	= xmalloc((INDEX) (MAXPATHLEN + 1));
    getfilename->direct 	= xmalloc((INDEX) (MAXPATHLEN + 1));
    getfilename->init_direct 	= xmalloc((INDEX) (MAXPATHLEN + 1));
    getfilename->fnmsg 		= xmalloc((INDEX) 80);
    getfilename->dummy_scrp 	= sm_create_scr(sm_scrmgr_screens());
    getfilename->store_skey	= smio_current_skey;
    getfilename->dir_only	= dir_only;
    getfilename->disallow_chdir = disallow_chdir;
    getfilename->master_data 	= NULL;
    getfilename->sub_data 	= NULL;
    getfilename->index		= 0;
    getfilename->window		= 0;

    if (msg && *msg)
	strcpy(getfilename->fnmsg, msg);
    else
	strcpy(getfilename->fnmsg, "Pick a file ");

    if (direct && *direct)
	strcpy(getfilename->prompt_direct, direct);
    else
	strcpy(getfilename->prompt_direct, "./");
    /* Expand ./, ~/, ~user/ etc */
    fqn(getfilename->direct, getfilename->prompt_direct); 
    if (*SM_LASTCHAR(getfilename->direct) != '/')
	strcat(getfilename->direct, "/");
    strcpy(getfilename->init_direct, getfilename->direct);
    strcpy(getfilename->prompt_direct, getfilename->direct);
    
    if (filename && *filename)
	strcpy(getfilename->file_mask, filename);
    else
	*getfilename->file_mask = 0;

    getfilename->sub_data = NULL;
    sm_add_callback(do_getfilename, getfilename);
    return(prep_getfilename(getfilename));
}

void sm_getfilename ARG2(CHAR *, filename, CHAR *, direct)
{
    CHAR	*p;

    p = strrchr(global.getfilename_filename, '/');
    if (p && *p) {
	*p = 0;
	strcpy(direct, global.getfilename_filename);
	strcpy(filename, p + 1);
	*p = '/';
    } else {
	strcpy(filename, global.getfilename_filename);
	strcpy(direct, ".");
    }
}

/* For emacs:
 * Local Variables:
 * tab-width: 8
 * End:
 */
